/** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ /** * @api */ define([ 'ko', 'underscore', 'mageUtils', 'uiRegistry', 'uiEvents', 'uiClass', './links', '../storage/local' ], function (ko, _, utils, registry, Events, Class, links) { 'use strict'; var Element; /** * Creates observable property using knockouts' * 'observableArray' or 'observable' methods, * depending on a type of 'value' parameter. * * @param {Object} obj - Object to whom property belongs. * @param {String} key - Key of the property. * @param {*} value - Initial value. */ function observable(obj, key, value) { var method = Array.isArray(value) ? 'observableArray' : 'observable'; if (_.isFunction(obj[key]) && !ko.isObservable(obj[key])) { return; } if (ko.isObservable(value)) { value = value(); } ko.isObservable(obj[key]) ? obj[key](value) : obj[key] = ko[method](value); } /** * Creates observable property using 'track' method. * * @param {Object} obj - Object to whom property belongs. * @param {String} key - Key of the property. * @param {*} value - Initial value. */ function accessor(obj, key, value) { if (_.isFunction(obj[key]) || ko.isObservable(obj[key])) { return; } obj[key] = value; if (!ko.es5.isTracked(obj, key)) { ko.track(obj, [key]); } } Element = _.extend({ defaults: { _requested: {}, containers: [], exports: {}, imports: {}, links: {}, listens: {}, name: '', ns: '${ $.name.split(".")[0] }', provider: '', registerNodes: true, source: null, statefull: {}, template: '', tracks: {}, storageConfig: { provider: 'localStorage', namespace: '${ $.name }', path: '${ $.storageConfig.provider }:${ $.storageConfig.namespace }' }, maps: { imports: {}, exports: {} }, modules: { storage: '${ $.storageConfig.provider }' } }, /** * Initializes model instance. * * @returns {Element} Chainable. */ initialize: function () { this._super() .initObservable() .initModules() .initStatefull() .initLinks() .initUnique(); return this; }, /** * Initializes observable properties. * * @returns {Element} Chainable. */ initObservable: function () { _.each(this.tracks, function (enabled, key) { if (enabled) { this.track(key); } }, this); return this; }, /** * Parses 'modules' object and creates * async wrappers for specified components. * * @returns {Element} Chainable. */ initModules: function () { _.each(this.modules, function (name, property) { if (name) { this[property] = this.requestModule(name); } }, this); if (!_.isFunction(this.source)) { this.source = registry.get(this.provider); } return this; }, /** * Called when current element was injected to another component. * * @param {Object} parent - Instance of a 'parent' component. * @returns {Collection} Chainable. */ initContainer: function (parent) { this.containers.push(parent); return this; }, /** * Initializes statefull properties * based on the keys of 'statefull' object. * * @returns {Element} Chainable. */ initStatefull: function () { _.each(this.statefull, function (path, key) { if (path) { this.setStatefull(key, path); } }, this); return this; }, /** * Initializes links between properties. * * @returns {Element} Chainbale. */ initLinks: function () { return this.setListeners(this.listens) .setLinks(this.links, 'imports') .setLinks(this.links, 'exports') .setLinks(this.exports, 'exports') .setLinks(this.imports, 'imports'); }, /** * Initializes listeners of the unique property. * * @returns {Element} Chainable. */ initUnique: function () { var update = this.onUniqueUpdate.bind(this), uniqueNs = this.uniqueNs; this.hasUnique = this.uniqueProp && uniqueNs; if (this.hasUnique) { this.source.on(uniqueNs, update, this.name); } return this; }, /** * Makes specified property to be stored automatically. * * @param {String} key - Name of the property * that will be stored. * @param {String} [path=key] - Path to the property in storage. * @returns {Element} Chainable. */ setStatefull: function (key, path) { var link = {}; path = !_.isString(path) || !path ? key : path; link[key] = this.storageConfig.path + '.' + path; this.setLinks(link, 'imports') .setLinks(link, 'exports'); return this; }, /** * Updates property specified in uniqueNs * if elements' unique property is set to 'true'. * * @returns {Element} Chainable. */ setUnique: function () { var property = this.uniqueProp; if (this[property]()) { this.source.set(this.uniqueNs, this.name); } return this; }, /** * Creates 'async' wrapper for the specified component * using uiRegistry 'async' method and caches it * in a '_requested' components object. * * @param {String} name - Name of requested component. * @returns {Function} Async module wrapper. */ requestModule: function (name) { var requested = this._requested; if (!requested[name]) { requested[name] = registry.async(name); } return requested[name]; }, /** * Returns path to elements' template. * * @returns {String} */ getTemplate: function () { return this.template; }, /** * Checks if template was specified for an element. * * @returns {Boolean} */ hasTemplate: function () { return !!this.template; }, /** * Returns value of the nested property. * * @param {String} path - Path to the property. * @returns {*} Value of the property. */ get: function (path) { return utils.nested(this, path); }, /** * Sets provided value as a value of the specified nested property. * Triggers changes notifications, if value has mutated. * * @param {String} path - Path to property. * @param {*} value - New value of the property. * @returns {Element} Chainable. */ set: function (path, value) { var data = this.get(path), diffs; diffs = !_.isFunction(data) && !this.isTracked(path) ? utils.compare(data, value, path) : false; utils.nested(this, path, value); if (diffs) { this._notifyChanges(diffs); } return this; }, /** * Removes nested property from the object. * * @param {String} path - Path to the property. * @returns {Element} Chainable. */ remove: function (path) { var data = utils.nested(this, path), diffs; if (_.isUndefined(data) || _.isFunction(data)) { return this; } diffs = utils.compare(data, undefined, path); utils.nestedRemove(this, path); this._notifyChanges(diffs); return this; }, /** * Creates observable properties for the current object. * * If 'useTrack' flag is set to 'true' then each property will be * created with a ES5 get/set accessor descriptors, instead of * making them an observable functions. * See 'knockout-es5' library for more information. * * @param {Boolean} [useAccessors=false] - Whether to create an * observable function or to use property accesessors. * @param {(Object|String|Array)} properties - List of observable properties. * @returns {Element} Chainable. * * @example Sample declaration and equivalent knockout methods. * this.key = 'value'; * this.array = ['value']; * * this.observe(['key', 'array']); * => * this.key = ko.observable('value'); * this.array = ko.observableArray(['value']); * * @example Another syntaxes of the previous example. * this.observe({ * key: 'value', * array: ['value'] * }); */ observe: function (useAccessors, properties) { var model = this, trackMethod; if (typeof useAccessors !== 'boolean') { properties = useAccessors; useAccessors = false; } trackMethod = useAccessors ? accessor : observable; if (_.isString(properties)) { properties = properties.split(' '); } if (Array.isArray(properties)) { properties.forEach(function (key) { trackMethod(model, key, model[key]); }); } else if (typeof properties === 'object') { _.each(properties, function (value, key) { trackMethod(model, key, value); }); } return this; }, /** * Delegates call to 'observe' method but * with a predefined 'useAccessors' flag. * * @param {(String|Array|Object)} properties - List of observable properties. * @returns {Element} Chainable. */ track: function (properties) { this.observe(true, properties); return this; }, /** * Checks if specified property is tracked. * * @param {String} property - Property to be checked. * @returns {Boolean} */ isTracked: function (property) { return ko.es5.isTracked(this, property); }, /** * Invokes subscribers for the provided changes. * * @param {Object} diffs - Object with changes descriptions. * @returns {Element} Chainable. */ _notifyChanges: function (diffs) { diffs.changes.forEach(function (change) { this.trigger(change.path, change.value, change); }, this); _.each(diffs.containers, function (changes, name) { var value = utils.nested(this, name); this.trigger(name, value, changes); }, this); return this; }, /** * Extracts all stored data and sets it to element. * * @returns {Element} Chainable. */ restore: function () { var ns = this.storageConfig.namespace, storage = this.storage(); if (storage) { utils.extend(this, storage.get(ns)); } return this; }, /** * Stores value of the specified property in components' storage module. * * @param {String} property * @param {*} [data=this[property]] * @returns {Element} Chainable. */ store: function (property, data) { var ns = this.storageConfig.namespace, path = utils.fullPath(ns, property); if (arguments.length < 2) { data = this.get(property); } this.storage('set', path, data); return this; }, /** * Extracts specified property from storage. * * @param {String} [property] - Name of the property * to be extracted. If not specified then all of the * stored will be returned. * @returns {*} */ getStored: function (property) { var ns = this.storageConfig.namespace, path = utils.fullPath(ns, property), storage = this.storage(), data; if (storage) { data = storage.get(path); } return data; }, /** * Removes stored property. * * @param {String} property - Property to be removed from storage. * @returns {Element} Chainable. */ removeStored: function (property) { var ns = this.storageConfig.namespace, path = utils.fullPath(ns, property); this.storage('remove', path); return this; }, /** * Destroys current instance along with all of its' children. * @param {Boolean} skipUpdate - skip collection update when element to be destroyed. */ destroy: function (skipUpdate) { this._dropHandlers() ._clearRefs(skipUpdate); }, /** * Removes events listeners. * @private * * @returns {Element} Chainable. */ _dropHandlers: function () { this.off(); if (_.isFunction(this.source)) { this.source().off(this.name); } else if (this.source) { this.source.off(this.name); } return this; }, /** * Removes all references to current instance and * calls 'destroy' method on all of its' children. * @private * @param {Boolean} skipUpdate - skip collection update when element to be destroyed. * * @returns {Element} Chainable. */ _clearRefs: function (skipUpdate) { registry.remove(this.name); this.containers.forEach(function (parent) { parent.removeChild(this, skipUpdate); }, this); return this; }, /** * Overrides 'EventsBus.trigger' method to implement events bubbling. * * @param {...*} arguments - Any number of arguments that should be passed to the events' handler. * @returns {Boolean} False if event bubbling was canceled. */ bubble: function () { var args = _.toArray(arguments), bubble = this.trigger.apply(this, args), result; if (!bubble) { return false; } this.containers.forEach(function (parent) { result = parent.bubble.apply(parent, args); if (result === false) { bubble = false; } }); return !!bubble; }, /** * Callback which fires when property under uniqueNs has changed. */ onUniqueUpdate: function (name) { var active = name === this.name, property = this.uniqueProp; this[property](active); }, /** * Clean data form data source. * * @returns {Element} */ cleanData: function () { if (this.source && this.source.componentType === 'dataSource') { if (this.elems) { _.each(this.elems(), function (val) { val.cleanData(); }); } else { this.source.remove(this.dataScope); } } return this; }, /** * Fallback data. */ cacheData: function () { this.cachedComponent = utils.copy(this); }, /** * Update configuration in component. * * @param {*} oldValue * @param {*} newValue * @param {String} path - path to value. * @returns {Element} */ updateConfig: function (oldValue, newValue, path) { var names = path.split('.'), index = _.lastIndexOf(names, 'config') + 1; names = names.splice(index, names.length - index).join('.'); this.set(names, newValue); return this; } }, Events, links); return Class.extend(Element); });